﻿//-----------------------------------------------------------------------
// <copyright file="ExceptionExtensions.cs" company="CodePlex">
//     Copyright (c) Doug Holland, CodePlex. All rights reserved.
// </copyright>
//-----------------------------------------------------------------------
namespace CodePlex.Diagnostics
{
    using System;
    
    using System.Collections;
    using System.Collections.Generic;

    using System.Diagnostics;

    using System.Reflection;

    using System.Security;
    using System.Security.Principal;

    using System.Threading;
    
    using System.Xml;
    using System.Xml.Linq;
    
    using CodePlex.Diagnostics.Contracts;   

    /// <summary>
    /// Provides extension methods for the <see cref="T:System.Exception"/> class to enable the publishing of exceptions using the CodePlex.Diagnostics framework.
    /// </summary>
    public static class ExceptionExtensions
    {
        #region public static Guid Publish(this Exception ex)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        public static Guid Publish(this Exception ex)
        {
            // given that an identity has not been specified the current Windows identity will be used to publish the exception.
            IIdentity identity = WindowsIdentity.GetCurrent() as IIdentity;

            return Publish(ex, identity);
        }

        #endregion

        #region public static Guid Publish(this Exception ex, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        public static Guid Publish(this Exception ex, IIdentity identity)
        {
           Dictionary<string, string> dictionary = new Dictionary<string, string>();
           
           return Publish(ex, dictionary, identity); 
        }

        #endregion
        
        #region public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="dictionary">A <see cref="T:System.Collections.Generic.Dictionary`2"/> containing name value pairs associated with this exception.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, IIdentity identity)
        {
            return Publish(ex, dictionary, null, identity);
        }
        
        #endregion

        #region public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, XElement element, IIdentity identity)

        /// <summary>
        /// Publishes the <see cref="T:System.Exception"/> using the configured exception provider.
        /// </summary>
        /// <param name="ex">The <see cref="T:System.Exception"/> to be published.</param>
        /// <param name="dictionary">A <see cref="T:System.Collections.Generic.Dictionary`2"/> containing name value pairs associated with this exception.</param>
        /// <param name="element">A <see cref="T:System.Xml.Linq.XElement"/> representing an xml element to be associated with this exception.</param>
        /// <param name="identity">The <see cref="T:System.Security.Principal.IIdentity"/> to associate with this published exception.</param>
        /// <returns>A <see cref="T:System.Guid"/> representing the unique publication identifier for an exception and associated inner exceptions.</returns>
        public static Guid Publish(this Exception ex, Dictionary<string, string> dictionary, XElement element, IIdentity identity)
        {
            // determine if the exception has already been published.
            PublishedException publishedException = ex as PublishedException;

            if (publishedException != null)
            {
                // exception has already been published, return the publication identifier.
                return publishedException.PublicationId;
            }

            // establish an exception data contract based upon the supplied exception object.
            ExceptionDataContract exceptionDataContract = new ExceptionDataContract(ex);

            exceptionDataContract.Unhandled = IsUnhandledException();

            // establish an identity data contract based upon the supplied identity object.
            IIdentityDataContract identityDataContract = new IIdentityDataContract(identity);

            // establish the application domain data contract based upon the current domain.
            exceptionDataContract.AppDomain = new AppDomainDataContract(AppDomain.CurrentDomain);

            // establish the assembly data contract based upon the calling assembly, that is the assembly containing the method calling this method.
            exceptionDataContract.Assembly = new AssemblyDataContract(Assembly.GetCallingAssembly());

            // establish the machine data contract.
            exceptionDataContract.Machine = new MachineDataContract();

            // establish the process data contract based upon the current process.
            exceptionDataContract.Process = new ProcessDataContract(Process.GetCurrentProcess());

            // establish the thread data contract based upon the current thread.
            exceptionDataContract.Thread = new ThreadDataContract(Thread.CurrentThread);

            // establish a proxy to the diagnostics service.
            DiagnosticsServiceClient client = new DiagnosticsServiceClient();

            // publish the exception using the diagnostics service, returning the publication identifier.
            IAsyncResult result = client.BeginPublishException(exceptionDataContract, dictionary, element, identityDataContract, null, null);

            // complete the publication of the exception and establish the publication Id.
            Guid publicationId = client.EndPublishException(result);

            // close the client.
            client.Close();

            return publicationId;
        }

        #endregion

        #region private static bool IsUnhandledException()

        /// <summary>
        /// Determines whether the <see cref="T:System.Exception"/> was published within an event handler for the <see cref="M:System.AppDomain.UnhandledException"/> event.
        /// </summary>
        /// <remarks>
        /// See <see href="http://msdn.microsoft.com/en-us/library/system.appdomain.unhandledexception.aspx"/> for more information about unhandled exceptions and 
        /// how and when the <see cref="M:System.AppDomain.UnhandledException"/> event is raised.
        /// </remarks>
        /// <returns>
        /// <c>true</c> if the <see cref="T:System.Exception"/> was published within an event handler for the <see cref="M:System.AppDomain.UnhandledException"/> event; otherwise, <c>false</c>.
        /// </returns>
        private static bool IsUnhandledException()
        {
            // establish the current stack trace.
            StackTrace trace = new StackTrace();

            // establish the stack frames within the stack trace.
            StackFrame[] frames = trace.GetFrames();

            MethodBase method = null;

            ParameterInfo[] parameters = null;

            for (int index = frames.Length - 1; index >= 0; index--)
            {
                // determine the method executed within the stack frame.
                method = frames[index].GetMethod();

                // determine the parameters for the method executed within the stack frame.
                parameters = method.GetParameters();

                // determine if the parameters for the method 
                if (parameters.Length == 2 && parameters[0].ParameterType == typeof(object) && parameters[1].ParameterType == typeof(UnhandledExceptionEventArgs))
                {
                    return true;
                }
            }

            return false;
        }

        #endregion
    }
}